package org.jpropelleralt.ref.impl;

import java.awt.Color;
import java.util.IdentityHashMap;
import java.util.Map;

import org.jpropelleralt.box.Box;
import org.jpropelleralt.box.BoxFeatures;
import org.jpropelleralt.box.impl.BoxFeaturesDefault;
import org.jpropelleralt.change.Change;
import org.jpropelleralt.change.ChangeDefault;
import org.jpropelleralt.multiverse.Multiverse;
import org.jpropelleralt.reaction.Reaction;
import org.jpropelleralt.ref.Ref;
import org.jpropelleralt.universe.Universe;

/**
 * Default implementation of {@link Ref}
 *
 * @param <T>		Type of value
 */
public class RefDefault<T> implements Ref<T> {

	private final Class<T> valueClass;
	private T value;
	private final BoxFeatures features;
	private final RefReaction reaction;
	private final boolean writeable;

	private RefDefault(Universe universe, Class<T> valueClass, T value) {
		this(universe, valueClass, value, true);
	}

	private RefDefault(Universe universe, Class<T> valueClass, T value, boolean writeable) {
		this.features = new BoxFeaturesDefault(this, universe);
		this.valueClass = valueClass;
		this.reaction = new RefReaction();
		this.writeable = writeable;
		swapValues(value);
	}
	
	@Override
	public BoxFeatures features() {
		return features;
	}
	
	@Override
	public T get() {
		Multiverse.prepareRead(this);
		try {
			return value;
		} finally {
			Multiverse.concludeRead(this);
		}
	}

	@Override
	public void set(T value) {
		if (!writeable) {
			throw new UnsupportedOperationException("This Ref is not writeable");
		}
		Multiverse.prepareWrite(this);
		try {
			swapValues(value);
			Multiverse.propagateWrite(this, ChangeDefault.shallowInstance());
		} finally {
			Multiverse.concludeWrite(this);
		}
	}
	
	@Override
	public void reactionSet(T t, boolean propagate) {
		if (propagate) {
			set(t);
		} else {
			swapValues(t);
		}
	};

	//Change over to a new value
	private void swapValues(T newValue) {
		
		//Do nothing if same instance
		if (newValue == value) {
			return;
		}
		
		T oldValue = value;

		value = newValue;
		
		if (oldValue instanceof Box) {
			((Box) oldValue).features().removeReaction(reaction);
		}
		if (newValue instanceof Box) {
			((Box) newValue).features().addReaction(reaction);
		}
	}

	@Override
	public Class<T> valueClass() {
		return valueClass;
	}
	
	/**
	 * A {@link Reaction} that just makes sure the {@link RefDefault}
	 * changes when its contents do.
	 */
	private class RefReaction implements Reaction {

		Map<Box, Change> defaultChanges = new IdentityHashMap<Box, Change>();
		
		private RefReaction() {
			defaultChanges.put(RefDefault.this, ChangeDefault.deepInstance());
		}
		
		@Override
		public void apply(boolean propagate) {
			//Nothing to do when we apply - we just propagate changes
		}
		@Override
		public boolean completeChanges(Map<Box, Change> changes) {
			//Nothing to do
			return false;
		}

		@Override
		public Map<Box, Change> react(Map<Box, Change> changes) {
			//We only listen to values of the RefDefault, so
			//if we see any change, it indicates a deep change to
			//RefDefault
			return defaultChanges;
		}
		
	}
	
	@Override
	public String toString() {
		return "Ref[" + get() + "]";
	}
	
	// Generic create methods
	
	/**
	 * Create a {@link RefDefault} with
	 * null value, and no value class.
	 * @param <T> 			The type of value
	 * @return 				A new {@link RefDefault}
	 */
	public static <T> RefDefault<T> create() {
		return create(Multiverse.getCurrentUniverse(), null, null);
	}
	
	/**
	 * Create a {@link RefDefault} with 
	 * no value class.
	 * @param <T> 			The type of value
	 * @param value		Initial value 
	 * @return 				A new {@link RefDefault}
	 */
	public static <T> RefDefault<T> create(T value) {
		return create(Multiverse.getCurrentUniverse(), null, value);
	}

	/**
	 * Create a {@link RefDefault}
	 * @param <T> 			The type of value
	 * @param valueClass	Value class 
	 * @param value			Initial value 
	 * @return 				A new {@link RefDefault}
	 */
	public static <T> RefDefault<T> create(Class<T> valueClass, T value) {
		return create(Multiverse.getCurrentUniverse(), valueClass, value);
	}

	/**
	 * Create a {@link RefDefault}
	 * @param <T> 			The type of value
	 * @param universe		The {@link Universe} we will belong to
	 * @param valueClass	Value class 
	 * @param value			Initial value 
	 * @return 				A new {@link RefDefault}
	 */
	public static <T> RefDefault<T> create(Universe universe, Class<T> valueClass, T value) {
		return new RefDefault<T>(universe, valueClass, value);
	}
	
	/**
	 * Create a {@link RefDefault} with no value class
	 * @param <T> 			The type of value
	 * @param universe		The {@link Universe} we will belong to
	 * @param value			Initial value 
	 * @return 				A new {@link RefDefault}
	 */
	public static <T> RefDefault<T> create(Universe universe, T value) {
		return new RefDefault<T>(universe, null, value);
	}
	
	/**
	 * Create a {@link RefDefault}
	 * @param <T> 			The type of value
	 * @param universe		The {@link Universe} we will belong to
	 * @param valueClass	Value class 
	 * @param value			Initial value 
	 * @param writeable		Whether the value is writeable
	 * @return 				A new {@link RefDefault}
	 */
	public static <T> RefDefault<T> create(Universe universe, Class<T> valueClass, T value, boolean writeable) {
		return new RefDefault<T>(universe, valueClass, value, writeable);
	}
	
	////////AUTO
// AutoCode for Boolean

	/**
	 * Create a {@link RefDefault} for
	 * Boolean values, in the universe
	 * given by {@link Multiverse#getCurrentUniverse()}.
	 * @param value		Initial value 
	 * @return			A new RefDefault
	 */
	public static RefDefault<Boolean> create(Boolean value) {
		return create(Boolean.class, value);
	}

	/**
	 * Create a {@link RefDefault} for
	 * Boolean values.
	 * @param universe		The {@link Universe} we will belong to
	 * @param value			Initial value 
	 * @return				A new RefDefault
	 */
	public static RefDefault<Boolean> create(Universe universe, Boolean value) {
		return create(universe, Boolean.class, value);
	}

// End of AutoCode for Boolean


// AutoCode for Byte

	/**
	 * Create a {@link RefDefault} for
	 * Byte values, in the universe
	 * given by {@link Multiverse#getCurrentUniverse()}.
	 * @param value		Initial value 
	 * @return			A new RefDefault
	 */
	public static RefDefault<Byte> create(Byte value) {
		return create(Byte.class, value);
	}

	/**
	 * Create a {@link RefDefault} for
	 * Byte values.
	 * @param universe		The {@link Universe} we will belong to
	 * @param value			Initial value 
	 * @return				A new RefDefault
	 */
	public static RefDefault<Byte> create(Universe universe, Byte value) {
		return create(universe, Byte.class, value);
	}

// End of AutoCode for Byte


// AutoCode for Short

	/**
	 * Create a {@link RefDefault} for
	 * Short values, in the universe
	 * given by {@link Multiverse#getCurrentUniverse()}.
	 * @param value		Initial value 
	 * @return			A new RefDefault
	 */
	public static RefDefault<Short> create(Short value) {
		return create(Short.class, value);
	}

	/**
	 * Create a {@link RefDefault} for
	 * Short values.
	 * @param universe		The {@link Universe} we will belong to
	 * @param value			Initial value 
	 * @return				A new RefDefault
	 */
	public static RefDefault<Short> create(Universe universe, Short value) {
		return create(universe, Short.class, value);
	}

// End of AutoCode for Short


// AutoCode for Integer

	/**
	 * Create a {@link RefDefault} for
	 * Integer values, in the universe
	 * given by {@link Multiverse#getCurrentUniverse()}.
	 * @param value		Initial value 
	 * @return			A new RefDefault
	 */
	public static RefDefault<Integer> create(Integer value) {
		return create(Integer.class, value);
	}

	/**
	 * Create a {@link RefDefault} for
	 * Integer values.
	 * @param universe		The {@link Universe} we will belong to
	 * @param value			Initial value 
	 * @return				A new RefDefault
	 */
	public static RefDefault<Integer> create(Universe universe, Integer value) {
		return create(universe, Integer.class, value);
	}

// End of AutoCode for Integer


// AutoCode for Long

	/**
	 * Create a {@link RefDefault} for
	 * Long values, in the universe
	 * given by {@link Multiverse#getCurrentUniverse()}.
	 * @param value		Initial value 
	 * @return			A new RefDefault
	 */
	public static RefDefault<Long> create(Long value) {
		return create(Long.class, value);
	}

	/**
	 * Create a {@link RefDefault} for
	 * Long values.
	 * @param universe		The {@link Universe} we will belong to
	 * @param value			Initial value 
	 * @return				A new RefDefault
	 */
	public static RefDefault<Long> create(Universe universe, Long value) {
		return create(universe, Long.class, value);
	}

// End of AutoCode for Long


// AutoCode for Float

	/**
	 * Create a {@link RefDefault} for
	 * Float values, in the universe
	 * given by {@link Multiverse#getCurrentUniverse()}.
	 * @param value		Initial value 
	 * @return			A new RefDefault
	 */
	public static RefDefault<Float> create(Float value) {
		return create(Float.class, value);
	}

	/**
	 * Create a {@link RefDefault} for
	 * Float values.
	 * @param universe		The {@link Universe} we will belong to
	 * @param value			Initial value 
	 * @return				A new RefDefault
	 */
	public static RefDefault<Float> create(Universe universe, Float value) {
		return create(universe, Float.class, value);
	}

// End of AutoCode for Float


// AutoCode for Double

	/**
	 * Create a {@link RefDefault} for
	 * Double values, in the universe
	 * given by {@link Multiverse#getCurrentUniverse()}.
	 * @param value		Initial value 
	 * @return			A new RefDefault
	 */
	public static RefDefault<Double> create(Double value) {
		return create(Double.class, value);
	}

	/**
	 * Create a {@link RefDefault} for
	 * Double values.
	 * @param universe		The {@link Universe} we will belong to
	 * @param value			Initial value 
	 * @return				A new RefDefault
	 */
	public static RefDefault<Double> create(Universe universe, Double value) {
		return create(universe, Double.class, value);
	}

// End of AutoCode for Double


// AutoCode for String

	/**
	 * Create a {@link RefDefault} for
	 * String values, in the universe
	 * given by {@link Multiverse#getCurrentUniverse()}.
	 * @param value		Initial value 
	 * @return			A new RefDefault
	 */
	public static RefDefault<String> create(String value) {
		return create(String.class, value);
	}

	/**
	 * Create a {@link RefDefault} for
	 * String values.
	 * @param universe		The {@link Universe} we will belong to
	 * @param value			Initial value 
	 * @return				A new RefDefault
	 */
	public static RefDefault<String> create(Universe universe, String value) {
		return create(universe, String.class, value);
	}

// End of AutoCode for String


// AutoCode for Color

	/**
	 * Create a {@link RefDefault} for
	 * Color values, in the universe
	 * given by {@link Multiverse#getCurrentUniverse()}.
	 * @param value		Initial value 
	 * @return			A new RefDefault
	 */
	public static RefDefault<Color> create(Color value) {
		return create(Color.class, value);
	}

	/**
	 * Create a {@link RefDefault} for
	 * Color values.
	 * @param universe		The {@link Universe} we will belong to
	 * @param value			Initial value 
	 * @return				A new RefDefault
	 */
	public static RefDefault<Color> create(Universe universe, Color value) {
		return create(universe, Color.class, value);
	}

// End of AutoCode for Color




}
